
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: MethodFinder.java 206 2009-11-16 21:53:48Z archie.cobbs $
 */

package org.dellroad.sidekar.util;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Utility class for finding all methods of a class and its superclasses, including "Miranda" methods
 * (methods declared in some (super)interface but not declared in any (super)class), but not including
 * "override duplicates", i.e., methods declared in a superclass or interface and also declared in some
 * (lower) class.
 *
 * <p>
 * The result is like {@link Class#getMethods()}, but including non-public methods as well.
 */
public class MethodFinder {

    private final HashSet<MethodHolder> methods = new HashSet<MethodHolder>();

    /**
     * Constructor. The method search is performed immediately when this constructor is invoked.
     *
     * @param clazz the class within which to find methods
     */
    public MethodFinder(Class<?> clazz) {

        // List class hierarchy top to bottom
        ArrayList<Class<?>> hier = new ArrayList<Class<?>>();
        for (Class<?> sc = clazz; sc != null; sc = sc.getSuperclass())
            hier.add(sc);
        Collections.reverse(hier);

        // Find all (indirectly) implemented interfaces
        HashSet<Class<?>> interfaces = new HashSet<Class<?>>();
        for (Class<?> sc = clazz; sc != null; sc = sc.getSuperclass())
            addInterfaces(interfaces, sc);

        // Add all methods declared in interfaces
        for (Class<?> iface : interfaces) {
            for (Method method : iface.getDeclaredMethods())
                this.methods.add(new MethodHolder(method));
        }

        // Add all methods declared in classes, top to bottom, so that
        // overridden methods in interfaces and superclasses get replaced
        for (Class<?> sc : hier) {
            for (Method method : sc.getDeclaredMethods()) {
                this.methods.remove(new MethodHolder(method));
                this.methods.add(new MethodHolder(method));
            }
        }
    }

    /**
     * Get all methods found by this instance.
     */
    public Set<Method> getMethods() {
        HashSet<Method> set = new HashSet<Method>();
        for (MethodHolder mh : this.methods)
            set.add(mh.getMethod());
        return set;
    }

    /**
     * Remove the given method from the set of methods previously found by this instance.
     */
    public void removeMethod(Method method) {
        this.methods.remove(new MethodHolder(method));
    }

    /**
     * Determine if any abstract methods were found.
     */
    public boolean containsAbstractMethod() {
        for (MethodHolder mh : this.methods) {
            if ((mh.getMethod().getModifiers() & Modifier.ABSTRACT) != 0)
                return true;
        }
        return false;
    }

    private void addInterfaces(Set<Class<?>> set, Class<?> c) {
        for (Class<?> iface : c.getInterfaces()) {
            set.add(iface);
            addInterfaces(set, iface);
        }
    }

    // Method container that matches overridden methods
    static final class MethodHolder {

        private final Method method;

        public MethodHolder(Method method) {
            this.method = method;
        }

        public Method getMethod() {
            return this.method;
        }

        @Override
        public int hashCode() {
            int hash = this.method.getName().hashCode();
            hash ^= this.method.getReturnType().hashCode();
            for (Class<?> ptype : this.method.getParameterTypes())
                hash ^= ptype.hashCode();
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof MethodHolder))
                return false;
            MethodHolder that = (MethodHolder)obj;
            if (!this.method.getName().equals(that.method.getName()))
                return false;
            if (!this.method.getReturnType().equals(that.method.getReturnType()))
                return false;
            if (!Arrays.equals(this.method.getParameterTypes(), that.method.getParameterTypes()))
                return false;
            return true;
        }
    }
}

